"""
全局命名空间中当前可用的模块显示在主窗口的左侧侧边栏中。从这个标签，你
可以探索任何模块可用的功能，这非常有助于学习python和numpy生态系统的
新部分。

可以将Modules选项卡中列出的任何对象拖放到节点编辑器中。这是一种不需要
输入任何代码就可以实例化新对象的便捷方法。

右键单击Modules选项卡的空白部分，将弹出一个可用选项的菜单。当你添加一个
文件或文件夹时，它们像任何其他模块一样在全局命名空间中是可用的。

导入模块和添加文件的区别在于当一个文件被添加到Modules选项卡时，该文件的
每一行都是被执行的，并且保存到该文件中的任何更改都被更新在节点编辑器中，
以及所有其他视图中。

要直接访问全局对象，该对象在全局命名空间中存储所有模块的名称和值，请使用该命令

>>> app.modules

有关可用属性的完整列表，请参阅“ModuleTree”类。

"""
import re, os, inspect, uuid, importlib.util, sys, traceback
from util import vdict
from widget import Treeview
from util import *
from tkinter import filedialog
from style import Color, getimage
from importlib import reload
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from console.builtin_print import builtin_print

class ModuleTree(vdict, Treeview):
	"""
	为了在视图之间同步可用模块，
	曲尺将所有模块的别名和值存储在一个称为模块树的vdict中。

	当一个文件或文件夹被添加到模块树中时，“watchdog”模块用于监控这
                些文件中的更改，并在所有视图中更新变量的值。

	从源代码执行代码是通过调用self.app.console.exec(source)完成的。
                请参阅“app.console”的文档。查看更多信息。
	"""
	def __init__(self, app):
		vdict.__init__(self, getitem=self.getmodule, setitem=self.setmodule, delitem=self.delete)
		Treeview.__init__(self, app)
		
		self.drag = None
		self.rootfolder = None
		self.handler = None
		self.observer = None
		self.watchers = {}
		self.locals = {}
		self.bind(BUTTON_RIGHT, lambda event: self.selection_set(self.identify_row(event.y)))
		self.bind(BUTTON_RELEASE_RIGHT, self._on_button_release_right)
		self.bind("<Double-Button-1>", self._on_double_button_1)

	def getmodule(self, key):
		if key not in self.store:
			for i in self.store:
				if self.store[i].__name__ == key or self.item(i)["values"][0] == key:
					return self.store[i]
		return self.store[key]

	def setmodule(self, name, module, parent=None, file=None):
		if not parent:
			if self.exists(module.__name__):
				self.delete(module.__name__)
			self.store[name] = module
			parent = self.insert("", 'end', module.__name__, 
				text="      " + name, 
				values=name, 
				image=getimage(".py"),
				open=True,
				tags="file" if file else "module")
			
		if len(self.store) == 1:
			self.app.menu.setview("modules", True)

		functions = []
		builtin_fn = []
		classes = []
		constants = []
		submodules = []
		for fn in dir(module):
			try:
				attr = getattr(module, fn)
			except:
				fn = "_" + fn
				
			if fn[:1] != "_":
				if inspect.isclass(attr):
					classes.append(fn)
				elif inspect.isfunction(attr):
					functions.append(fn)
				elif callable(attr):
					builtin_fn.append(fn)
				elif inspect.ismodule(attr):
					if attr.__name__ not in EXCLUDED_MODULES:# + BUILTIN_PKGS + INSTALLED_PKGS:
						submodules.append((fn, attr))
				else:
					constants.append(fn)
		
		if builtin_fn:
			folder = self.insert(parent, "end", text="builtins")
			for i in builtin_fn:
				self.insert(folder, "end", text=i, values=module.__name__ + "." + i)

		if functions:
			folder = self.insert(parent, "end", text="functions")
			for i in functions:
				self.insert(folder, "end", text=i, values=module.__name__ + "." + i)

		if classes:
			folder = self.insert(parent, "end", text="classes")
			for j in classes:
				self.insert(folder, "end", text=j, values=module.__name__ + "." + j)

		if constants:
			folder = self.insert(parent, "end", text="objects")
			for k in constants:
				if not re.match(r"^[A-Z_]*$", k):
					self.insert(folder, "end", text=k, values=module.__name__ + "." + k)

		if submodules:
			for fn, attr in submodules:
				if not self.exists(attr.__name__):
					folder = self.insert(parent, "end", attr.__name__, text="      " + fn, values=attr.__name__, image=getimage(".py"))
					self.setmodule(attr.__name__, attr, folder)
		return False

	def delete(self, key, del_objs=True):
		if self.has_tag(key, "folder"):
			for i in self.get_children(key):
				self.delete(i)

		if self.exists(key):
			super(Treeview, self).delete(key)
		else:
			name, ext = name_ext(key)
			if self.exists(name):
				super(Treeview, self).delete(name)

		if del_objs and key in self.locals:
			for i in self.locals[key]:
				if i in self.app.objects:
					del self.app.objects[i]
			del self.locals[key]

			if key in self.watchers:
				if self.observer:
					# TODO: use a better system for unscheduling
					try:
						self.observer.unschedule(self.watchers[key])
					except:
						pass
				del self.watchers[key]
		
		if key in self.store:
			del self.store[key]
		return False

	def addfile(self, file=None, parent="", index="end", watch=True, is_open=True, exec_file=True):
		file = file or filedialog.askopenfilename()
		import os
		import sys
		sys.path.append(os.path.dirname(file))
		if not file: return

		name, ext = name_ext(file)		
		if os.path.basename(file) in EXCLUDED_FILES or ext in EXCLUDED_EXT: return

		if self.exists(name):
			self.delete(name, del_objs=False)
		
		parent = self.insert(parent if (self.rootfolder and parent != self.rootfolder) else "", index, name, 
			text="      " + name, 
			image=getimage(ext), 
			tags="local" if parent else ("local", "file"),
			open=is_open,
			values=file)
		
		if ext != ".py": return
		
		if name in sys.modules:
			del sys.modules[name]

		if watch:
			if not self.observer:
				self.observer = Observer()
				self.observer.start()
			
			self.watchers[name] = self.observer.schedule(FileHandler(self, file=file), 
				path=os.path.dirname(file), recursive=False)

		expanded = self.expanded()
		spec = importlib.util.spec_from_file_location(name, file)
		module = importlib.util.module_from_spec(spec)
		try:
			spec.loader.exec_module(module)
		except Exception as err:
			traceback.print_exc()
			self.disable_file(parent)
			return

		if exec_file:
			for i in dir(module):
				attr = getattr(module, i)
				if not inspect.ismodule(attr) and i in self.app.objects and i not in self.locals[name]:
					self.disable_file(parent)
					print (Exception("ImportError: object with name " + i + " already exists.\n  File \"" + file + "\", line 1, in <module>"))
					builtin_print ("\a")
					return	

		if name not in self.store:
			self.store[name] = module

		self.setmodule(name, module, parent, file)
		self.expanded(expanded)
		
		if exec_file:
			prev = self.app.objects.store.copy()
			#source = open(file, "r").read()
			

			new = self.app.objects.store.copy()

			if name in self.locals:
				self.locals[name].update({ k: new[k] for k in set(new) - set(prev) })
			else:
				self.locals[name] = { k: new[k] for k in set(new) - set(prev) }

			objects = [i for i in dir(module)]
			keys = list(self.locals[name].keys())
			for i in keys:
				if i not in objects:
					del self.locals[name][i]
					del self.app.objects[i]

			for i in objects:
				attr = getattr(module, i)
				if inspect.ismodule(attr):
					self.setmodule(i, attr, name)


	def addfolder(self, dirpath=None, parent="", watch=True, is_rootfolder=False, exec_file=True):
		dirpath = dirpath or filedialog.askdirectory(initialdir=".")
		if not dirpath: return

		name = os.path.basename(dirpath)
		parent = "" if parent == self.rootfolder else parent

		if is_rootfolder:
			self.rootfolder = dirpath
		else:
			parent = self.insert(parent, "end", dirpath, 
				text="      " + name, 
				open=(not parent), 
				image=getimage("folder"), 
				tag="folder" if not parent else "",
				values=dirpath)		
		
		items = [(i, os.path.isdir(os.path.join(dirpath, i))) for i in os.listdir(dirpath)]
		items.sort()
		for name, is_dir in items:
			if is_dir and name not in EXCLUDED_DIR:
				self.addfolder(os.path.join(dirpath, name), parent, watch=False, exec_file=exec_file)
		
		for name, is_dir in items:
			if not is_dir and name not in EXCLUDED_FILES:
				fullpath = os.path.join(dirpath, name)
				self.addfile(fullpath, parent, watch=False, exec_file=exec_file)

		if watch:
			if not self.observer:
				self.observer = Observer()
				self.observer.start()
			
			self.watchers[name] = self.observer.schedule(FileHandler(self, dirpath=dirpath), 
				path=dirpath, recursive=True)

	def stop_observer(self):
		if self.observer:
			self.observer.stop()
			self.observer = None

	def files(self, tag, recursive=False, key=""):
		children = self.get_children(key)
		if recursive is True:
			result = []
			if self.has_tag(key, tag):
				result.append(key)
			
			for i in children:
				result.extend(self.files(tag, True, i))
			return result
		
		keys = [j for j in children if self.has_tag(j, tag)]
		return [self.app.modules.item(i)["values"][0] for i in keys]

	def disable_file(self, key):
		self.item(key, image=getimage("python-disabled"))
		self.add_tag(key, "disabled")
		children = self.get_children(key)
		for i in children:
			self.delete(i)

	def _on_double_button_1(self, event):
		key = self.identify_row(event.y)
		if not key: return

		value = self.item(key)["values"]
		if not value: return
		value = value[0]

		if key in self and os.path.isfile(value):
			try:
				sourcefile = inspect.getsourcefile(self[key])
			except:
				sourcefile = None	

			if sourcefile:
				open_editor(sourcefile)
				return "break"

		obj = help.getobj(value)
		if obj:
			help(obj)
			return "break"
			
	def _on_button_release_right(self, event):
		key = self.identify_row(event.y)
		if not key:
			self.menu.set_menu(self.app.menu.project_menu)
			return self.menu.show(event)
		
		value = self.item(key)["values"][0]
		items = []
		obj = help.getobj(value)
		if obj is not None:
			items.append({
				"label": "帮助",
				"command": lambda: help(value)
			})

		try:
			file = inspect.getsourcefile(obj)
		except:
			file = None

		if file:
			items.append({
				"label": "查看源文件",
				"command": lambda: open_editor(inspect.getsourcefile(obj))
			})
		
		# print ('v', value)
		if key in self.get_children() and value != self.rootfolder:
			items.extend([{ 
				"separator": None 
			}, {
				"label": "删除 " + key,
				"command": lambda: self.delete(value)
			}])	
		
		self.menu.set_menu(items)
		self.menu.show(event)


class FileHandler(FileSystemEventHandler):
	def __init__(self, modules, file=None, dirpath=None):
		FileSystemEventHandler.__init__(self)
		self.modules = modules
		self.file = file

	def on_created(self, event):
		if self.is_excluded(event): return

		dirname = os.path.dirname(event.src_path)
		parent = "" if dirname == self.modules.rootfolder else os.path.basename(dirname)

		if event.is_directory:
			self.modules.addfolder(event.src_path, parent)
		else:
			self.modules.addfile(event.src_path, parent)

	def on_deleted(self, event):
		if self.is_excluded(event): return
		name, ext = name_ext(event.src_path)

		if event.is_directory:
			del self.modules[os.path.basename(event.src_path)]
		else:
			del self.modules[name]

	def on_modified(self, event):
		if self.is_excluded(event): return

		name, ext = name_ext(event.src_path)
		order = self.modules.order()
		expanded = self.modules.expanded()
		index = self.modules.index(name)

		dirname = os.path.dirname(event.src_path)
		parent = "" if (dirname == self.modules.rootfolder or name in self.modules) else dirname
		if parent:
			self.modules.addfile(event.src_path, parent, index=index, watch=False)
		else:
			self.modules.addfile(event.src_path, index=index, watch=False)
		self.modules.order(order)
		self.modules.expanded(expanded)

	def on_moved(self, event):
		name, ext = name_ext(event.src_path)
		self.modules.item(name, text="      " + os.path.basename(event.dest_path), values=event.dest_path)

	def is_excluded(self, event):
		name, ext = name_ext(event.src_path)
		return (self.file and self.file != event.src_path
			or event.is_directory 
			or (name + ext) in EXCLUDED_FILES
			or ext in EXCLUDED_EXT 
			or name in EXCLUDED_DIR
		)